Rime 用户词典
数据结构
- 采用 Google 开发的开源数据库 LevelDB 存储,是一个有序的键值数据库,可以想像成一个 C++ 的 TreeMap
- 键 Key:是编码和汉字,以
\t
分隔。例如"ming yue pin yin\t明月拼音"
,或者"ggtt\t五笔"
- 值 Value:是形如
c=? d=? t=?
的字符串,在 librime 中解析为UserDbValue
类型
struct UserDbValue {
int commits = 0;
double dee = 0.0;
uint64_t tick = 0;
}
commits
:上屏总次数dee
:当前的新鲜度(热乎度)tick
:这个词上次被使用的时候的时间戳
常规使用
d 公式
- 阐释:d 公式模拟了一个人对一个概念的指数遗忘规律。每过 200 个时间戳,用户对一个词的新鲜度折旧为原来的 37%。不过,如果用户使用了这个词一次,新鲜度就会增加 ,这个值通常是 1。
p 公式
阐释:p 公式是根据 , 和当前时间 来综合判断字词的排序的算法
- 代表了这个词的频率的贡献。 是过去上屏的所有字词中这个词占的比例,后面的部分是这个比例的置信度。如果 ,那么说明用户刚刚开始调教词库,这个时候经验频率的置信度不高,后面这个因子非常小,导致 也非常小;反之,如果 ,那么后面这个因子接近 1。
- 后面的部分代表了这个词的新鲜度的贡献。特别的,如果这个词的新鲜度非常高,那么需要以指数级别来强调这个词( 部分)
- 总之,如果用户刚开始使用,那么主要是新鲜度的贡献比较大;反之,是频率的贡献比较大。
条目创建
UserDictionary::UpdateEntry
函数负责新条目的创建,除了条目之外还接受参数commits
和new_entry_prefix
TableTranslator::Memorize
函数分析上屏历史,调用TableEncoder::DfsEncode
对已上屏字词枚举编码,然后再调用UnityTableEncoder::CreateEntry
函数,最后调用UserDictionary::UpdateEntry
,此时commits = 0
,new_entry_prefix
是"\177enc\037"
这个特殊的字符串用来标识这个字是自动造出来的词,价值不高。UserDbValue
被初始化为{ commits = 0, dee = 0.1, tick = t }
条目更新
条目更新也是调用上面的这个函数,但是调用的时候 commits = 1
。v.commits += 1
,v.dee
按照上面的函数更新。特别的,如果这个条目更新是确认了之前新建的词,则特殊字符串会被删除,标志着它正式进入词库
条目删除(假删除)
ExpressEditor 可以通过 Shift+Delete 或者 Control+Delete 等快捷键删除用户词典的词条,但是并没有真删除,实际上是调用了 UpdateEntry(entry, -1)
。此时 v.commits
会被加个负号作为标记(如果本来就是 0,则变成 -1),v.dee
也根据这个衰减。
声笔系列码新加了一个自动删除的功能,也会调用这个函数。
条目查询
查询的时候是用输入的编码在 LevelDB 中查找。因为 LevelDB 是有序的键值存储,所以在没有精确匹配的情况下,会找到按字典序比这个大的条目。这也就是为什么 LevelDB 里的键是 "编码 + \t + 字词",但是只用编码查询也能查询到的原因。一般情况下,LevelDB 中所有匹配这个前缀的编码都会返回,不过声笔系列码中做了特殊判断,遍历之后返回权重最高的作为 3 码的候选(以声笔简码为例)。
最后调用 p 公式,然后取对数再加上 1(不知道为什么这么做),作为候选项的权重
批量处理
使用工具程序 rime_dict_manager
来完成
- 需要关闭正在使用的输入法,释放词典文件
- 将工作目录设置为「Rime 用户文件夹」
备份、合并词典快照
- 备份:
--backup dict_name
- 合并:
--restore xxx.userdb.txt
合并时,词频和 dee 会更新为二者的较大值,tick 更新为全局的最新值
导入、导出文本码表
- 导入:
--import dict_name import.txt
- 导出:
--export dict_name export.txt
导入、导 出的格式为以制表符分隔的三列,分别为文字、编码、使用频次,但是文本码表因为没有 tick 和 dee 字段,所以包含的信息没有快照全面